//
// GPSMapEdit
// (c) Konstantin Galichsky (kg@geopainting.com), 2003-2007
//
// PCX5 tracks/waypoints parser.
//

# include "StdAfx.h"
# include "ParseHelpers.h"
# include "Map.h"
# include "Types.h"
# include "Load.h"
# include "Log.h"
# include "Status.h"
# include "fast_floor.h"

class CPCX5Loader : public CMapLoader {
  public:
	// Override.
	virtual bool IsValid (const char * _pData, size_t _cFileSize) const;
	virtual LoadStatus_t Load (const char * _pData, size_t _cFileSize, const char * _strFileName, map_t * _pMap);

  private:
	void ParseHeader     (const char * _strLine, const char * _strEnd);
	void ParseRoute      (const char * _strLine, const char * _strEnd);
	void ParseWaypoint   (const char * _strLine, const char * _strEnd);
	void ParseTrackpoint (const char * _strLine, const char * _strEnd);

	map_t * m_pMap;
	const char * m_pStart, * m_pEnd;

	//
	size_t m_cOrdinal;

	// Data for track.
	points_t m_points;
	trackpoints_t m_points_extra;
	size_t m_cTrackBeginPos;
	string_t m_strTrackLabel;
	string_t m_strDefaultTrackLabel;
	void SaveTrack (const char * _strTrackEnd, const char * _strEnd);

	// Data for route.
	points_t      * m_pRtePoints;
	routepoints_t * m_pRteRPoints;

	file_refs_t m_file_refs;

	typedef bool (* pfnParseCoordinate_t) (const char * _pValue, const char * _pEnd, const char ** _ppEnd, float & _fRet);
	pfnParseCoordinate_t m_pfnParseLat;
	pfnParseCoordinate_t m_pfnParseLon;

	size_t m_cNameOffset;
	size_t m_cLatOffset;
	size_t m_cLonOffset;
	size_t m_cDateOffset;
	size_t m_cTimeOffset;
	size_t m_cAltOffset;
	size_t m_cDescriptionOffset;
	size_t m_cProximityOffset;
	size_t m_cSymbolOffset;
};

class CPCX5TrkLoader : public CPCX5Loader {};
class CPCX5WptLoader : public CPCX5Loader {};
class CPCX5RteLoader : public CPCX5Loader {};

DECLARE_WPTRK_LOADER (CPCX5TrkLoader, "garmin-pcx5-trk", "trk", "PCX5 tracks (*.trk)",    ftiTracks);
DECLARE_WPTRK_LOADER (CPCX5WptLoader, "garmin-pcx5-wpt", "wpt", "PCX5 waypoints (*.wpt)", ftiWpts);
DECLARE_WPTRK_LOADER (CPCX5RteLoader, "garmin-pcx5-rte", "rte", "PCX5 routes (*.rte)",    ftiRoutes);

bool CPCX5Loader::IsValid (const char * _pData, size_t _cFileSize) const {
	if (! CheckAnsiFile (_pData, _cFileSize, true))
		return false;

	return HasPrefix (_pData, _pData + _cFileSize, "H ", 2);
}

void CPCX5Loader::ParseHeader (const char * _strLine, const char * _strEnd) {
	m_cNameOffset        = -1;
	m_cLatOffset         = -1;
	m_cLonOffset         = -1;
	m_cDateOffset        = -1;
	m_cTimeOffset        = -1;
	m_cAltOffset         = -1;
	m_cDescriptionOffset = -1;
	m_cProximityOffset   = -1;
	m_cSymbolOffset      = -1;

	const char * p = _strLine + 1;
	while (p < _strEnd) {
		OmitSpaces (& p, _strEnd);

		if (HasPrefix (p, _strEnd, "IDNT", 4))
			m_cNameOffset        = p - _strLine;
		else if (HasPrefix (p, _strEnd, "LATITUDE", 8))
			m_cLatOffset         = p - _strLine;
		else if (HasPrefix (p, _strEnd, "LONGITUDE", 9))
			m_cLonOffset         = p - _strLine;
		else if (HasPrefix (p, _strEnd, "DATE", 4))
			m_cDateOffset        = p - _strLine;
		else if (HasPrefix (p, _strEnd, "TIME", 4))
			m_cTimeOffset        = p - _strLine;
		else if (HasPrefix (p, _strEnd, "ALT", 3))
			m_cAltOffset         = p - _strLine;
		else if (HasPrefix (p, _strEnd, "DESCRIPTION", 11))
			m_cDescriptionOffset = p - _strLine;
		else if (HasPrefix (p, _strEnd, "PROXIMITY", 9))
			m_cProximityOffset   = p - _strLine;
		else if (HasPrefix (p, _strEnd, "SYMBOL", 6))
			m_cSymbolOffset      = p - _strLine;

		OmitNonSpaces (& p, _strEnd);
	}
}

static
bool _ParseDEGLat (const char * _pValue, const char * _pEnd, const char ** _ppEnd, float & _fRet) {
	const char * p = _pValue;
	OmitSpaces (& p, _pEnd);

	// Get sign.
	bool bMinus = false;
	switch (* p) {
		case 'N': break;
		case 'S': bMinus = true; break;
		default: return false;
	}
	++ p;

	// Get degrees.
	float fDeg = 0;
	if (! ParseReal (p, _pEnd, & p, fDeg))
		return false;

	* _ppEnd = p;
	_fRet = fDeg;
	if (bMinus)
		_fRet = - _fRet;
	return true;
}
 
static
bool _ParseDEGLon (const char * _pValue, const char * _pEnd, const char ** _ppEnd, float & _fRet) {
	const char * p = _pValue;
	OmitSpaces (& p, _pEnd);

	// Get sign.
	bool bMinus = false;
	switch (* p) {
		case 'E': break;
		case 'W': bMinus = true; break;
		default: return false;
	}
	++ p;

	// Get degrees.
	float fDeg = 0;
	if (! ParseReal (p, _pEnd, & p, fDeg))
		return false;

	* _ppEnd = p;
	_fRet = fDeg;
	if (bMinus)
		_fRet = - _fRet;
	return true;
}

static
bool _ParseDMLat (const char * _pValue, const char * _pEnd, const char ** _ppEnd, float & _fRet) {
	const char * p = _pValue;
	OmitSpaces (& p, _pEnd);

	// Get sign.
	bool bMinus = false;
	switch (* p) {
		case 'N': break;
		case 'S': bMinus = true; break;
		default: return false;
	}
	++ p;

	// Get degrees.
	const float fDeg = (p [0] - '0')*10 + (p [1] - '0');
	p += 2;

	// Get minutes.
	float fMin = 0;
	if (! ParseReal (p, _pEnd, & p, fMin))
		return false;

	* _ppEnd = p;
	_fRet = fDeg + fMin/60;
	if (bMinus)
		_fRet = - _fRet;
	return true;
}

static
bool _ParseDMLon (const char * _pValue, const char * _pEnd, const char ** _ppEnd, float & _fRet) {
	const char * p = _pValue;
	OmitSpaces (& p, _pEnd);

	// Get sign.
	bool bMinus = false;
	switch (* p) {
		case 'E': break;
		case 'W': bMinus = true; break;
		default: return false;
	}
	++ p;

	// Get degrees.
	const float fDeg = (p [0] - '0')*100 + (p [1] - '0')*10 + (p [2] - '0');
	p += 3;

	// Get minutes.
	float fMin = 0;
	if (! ParseReal (p, _pEnd, & p, fMin))
		return false;

	* _ppEnd = p;
	_fRet = fDeg + fMin/60;
	if (bMinus)
		_fRet = - _fRet;
	return true;
}

static const char * g_strMonths [12] = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"};

static
int _FindMonth (const char * _strMonth) {
	for (size_t c = 0; c < 12; ++ c)
		if (::_strnicmp (_strMonth, g_strMonths [c], 3) == 0)
			return c + 1;
	return -1;
}

static bool _ParseDateTime (const char * _strLine, const char * _strEnd, size_t _cDateOffset, size_t _cTimeOffset, DATE & _dt) {
	const char * const strDate = _strLine + _cDateOffset;

	const char * const strDay = strDate;
	const int nDay = ParseDecU (strDay, _strEnd);

	const char * const strMonth = strDate + 3;
	const int nMonth = _FindMonth (strMonth);
	if (nMonth == -1)
		return false;

	const char * const strYear = strDate + 7;
	int nYear = ParseDecU (strYear, _strEnd);
	if (nYear < 90)
		nYear += 2000;
	else
		nYear += 1900;

	const char * const strTime = _strLine + _cTimeOffset;
	const int nHour   = ParseDecU (strTime,     _strEnd);
	const int nMinute = ParseDecU (strTime + 3, _strEnd);
	const int nSecond = ParseDecU (strTime + 6, _strEnd);

	SYSTEMTIME st = {nYear, nMonth, 0, nDay, nHour, nMinute, nSecond, 0};
	if (! ::SystemTimeToVariantTime (& st, & _dt))
		return false;

	return true;
}

void CPCX5Loader::ParseRoute (const char * _strLine, const char * _strEnd) {
	const char * p = _strLine + 1;
	OmitSpaces (& p, _strEnd);

	const char * const strName = p;

	//
	// Add route to the map.
	//
	rgn_t & rgn = m_pMap->CreateRgn (rkPolyline);
	rgn.SetRouteType ();

	rgn.pFileRef  = & m_file_refs.back ();
	rgn.cBeginPos = _strLine - m_pStart;
	rgn.cEndPos   = _strEnd  - m_pStart;
	rgn.cFileLine = -1;
//	rgn.cOrdinal  = m_cOrdinal ++;

	rgn.strLabel.assign (strName, GetLineLen (strName, _strEnd));

	rgn.elements.push_back (rgn_element_t ());
	rgn_element_t & element = rgn.elements.back ();
	element.cFileOffset = rgn.cBeginPos;
	element.cLevel = 0;

	m_pRtePoints = & element.points;

	route_info_t * const pRteExtra = rgn.pExtra->wtr.CreateRoute ();
	m_pRteRPoints = & pRteExtra->points;
}

void CPCX5Loader::ParseWaypoint (const char * _strLine, const char * _strEnd) {
	const char * p = _strLine + 1;
	OmitSpaces (& p, _strEnd);

	const char * const strName        = _strLine + m_cNameOffset;
	const char * const strLat         = _strLine + m_cLatOffset;
	const char * const strLon         = _strLine + m_cLonOffset;
	const char * const strAltitude    = _strLine + m_cAltOffset;
	const char * const strDescription = _strLine + m_cDescriptionOffset;
	const char * const strProximity   = _strLine + m_cProximityOffset;
	const char * const strSymbol      = _strLine + m_cSymbolOffset;

	point_t point;
	if (
		! ParseReal     (strLat, _strEnd, & p, point.y) &&
		! m_pfnParseLat (strLat, _strEnd, & p, point.y)
	) {
		ReportWarning (_strLine - m_pStart, "Unexpected format of waypoint data.");
		return;
	}
	if (
		! ParseReal     (strLon, _strEnd, & p, point.x) &&
		! m_pfnParseLon (strLon, _strEnd, & p, point.x)
	) {
		ReportWarning (_strLine - m_pStart, "Unexpected format of waypoint data.");
		return;
	}

	//
	// Add the waypoint to the map.
	//
	rgn_t & rgn = m_pMap->CreateRgn (rkPoint);
	rgn.SetWaypointType ();

	rgn.pFileRef = & m_file_refs.back ();
	rgn.cBeginPos = _strLine - m_pStart;
	rgn.cEndPos = _strEnd - m_pStart;
	rgn.cFileLine = -1;
//	rgn.cOrdinal = m_cOrdinal ++;

	// Name.
	rgn.strLabel.assign (strName, GetLineLen (strName, strLat - 1));
	// Description.
	rgn.strComment.assign (strDescription, GetLineLen (strDescription, strProximity));

	// Position.
	rgn.origins.push_back (rgn_origin_t ());
	rgn_origin_t & origin = rgn.origins.back ();
	origin.cLevel = 0;
	origin.cFileOffset = _strLine - m_pStart;
	origin.point = point;

	// Extra properties.
	waypoint_info_t * const pWptExtra = rgn.pExtra->wtr.CreateWaypoint ();

	// Date and Time.
	if (_ParseDateTime (_strLine, _strEnd, m_cDateOffset, m_cTimeOffset, pWptExtra->dtTime))
		pWptExtra->wFlags |= waypoint_info_t::defTime;

	// Altitude.
	float fAltitude = 0;
	if (! ParseReal (strAltitude, _strEnd, & p, fAltitude))
		return;
	if (fAltitude > -1000) {
		pWptExtra->wFlags |= waypoint_info_t::defAltitude;
		pWptExtra->fAltitude = fAltitude;
	}

	// Proximity.
	float fProximity = 0;
	if (ParseReal (strProximity, _strEnd, & p, fProximity)) {
		// Parse E-order.
		if (* p == 'E' || * p == 'e') {
			++ p;

			bool bMinusOrder = false;
			if (* p == '+')
				++ p;
			else if (* p == '-') {
				++ p;
				bMinusOrder = true;
			}

			unsigned long ulOrder = 0;
			if (ParseDecU (p, _strEnd, & p, ulOrder)) {
				fProximity *= ::pow (10.f, bMinusOrder ? -(int)ulOrder : (int)ulOrder);
			}
		}

		if (fProximity > 0) {
			pWptExtra->wFlags |= waypoint_info_t::defProximity;
			pWptExtra->fProximity = fProximity;
		}
	}

	// Symbol.
	if (strSymbol < _strEnd) {
		const int nType = ParseDecU (strSymbol, _strEnd);
		// TODO:
	}

	//
	// Add to route, if any.
	//
	if (m_pRtePoints != NULL) {
		m_pRtePoints->push_back (point);

		assert (m_pRteRPoints != NULL);
		m_pRteRPoints->push_back (routepoint_info_t ());
		routepoint_info_t & rpoint = m_pRteRPoints->back ();
		rpoint.pWaypoint = & rgn;
	}
}

void CPCX5Loader::SaveTrack (const char * _strTrackEnd, const char * _strEnd) {
	//
	// Add the track to the map.
	//
	if (! m_points.empty ()) {
		rgn_t & rgn = m_pMap->CreateRgn (rkPolyline);
		rgn.SetTrackType ();

		rgn.pFileRef = & m_file_refs.back ();
		rgn.cBeginPos = m_cTrackBeginPos;
		rgn.cEndPos = _strTrackEnd - m_pStart;
		rgn.cFileLine = -1;
	//	rgn.cOrdinal = m_cOrdinal ++;

		rgn.strLabel = m_strTrackLabel;

		rgn.elements.push_back (rgn_element_t ());
		rgn_element_t & element = rgn.elements.back ();
		element.cFileOffset = m_cTrackBeginPos;

		element.cLevel = 0;
		element.points.swap (m_points);

		rgn.pExtra->wtr.CreateTrack ();
		element.pExtra->TrackPoints.swap (m_points_extra);
	}

	//
	// Parse next track label, if any.
	//
	if (_strTrackEnd < m_pEnd) {
		const char * p = _strTrackEnd + 1;
		OmitSpaces (& p, _strEnd);

		if (HasPrefix (p, _strEnd, "TN", 2)) {
			p += 2;
			OmitSpaces (& p, _strEnd);

			m_strTrackLabel.assign (p, GetLineLen (p, _strEnd));
		}
	}
	if (m_strTrackLabel.empty ())
		m_strTrackLabel = m_strDefaultTrackLabel;
}

void CPCX5Loader::ParseTrackpoint (const char * _strLine, const char * _strEnd) {
	const char * const strLat         = _strLine + m_cLatOffset;
	const char * const strLon         = _strLine + m_cLonOffset;
	const char * const strAltitude    = _strLine + m_cAltOffset;

	point_t point;

	// Position.
	const char * p;
	if (
		! ParseReal     (strLat, _strEnd, & p, point.y) &&
		! m_pfnParseLat (strLat, _strEnd, & p, point.y)
	) {
		ReportWarning (_strLine - m_pStart, "Unexpected format of trackpoint data.");
		return;
	}
	if (
		! ParseReal     (strLon, _strEnd, & p, point.x) &&
		! m_pfnParseLon (strLon, _strEnd, & p, point.x)
	) {
		ReportWarning (_strLine - m_pStart, "Unexpected format of trackpoint data.");
		return;
	}

	trackpoint_info_t point_extra;

	// Date and Time.
	if (_ParseDateTime (_strLine, _strEnd, m_cDateOffset, m_cTimeOffset, point_extra.dtTime))
		point_extra.wFlags |= trackpoint_info_t::defTime;

	// Altitude.
	float fAltitude = 0;
	if (! ParseReal (strAltitude, _strEnd, & p, fAltitude))
		return;
	if (fAltitude > -1000) {
		point_extra.wFlags |= trackpoint_info_t::defAltitude;
		point_extra.fAltitude = fAltitude;
	}

	m_points.push_back (point);
	m_points_extra.push_back (point_extra);
}

LoadStatus_t CPCX5Loader::Load (const char * _pData, size_t _cFileSize, const char * _strMapFileName, map_t * _pMap) {
	m_pMap = _pMap;
	m_pStart = _pData;
	m_pEnd = _pData + _cFileSize;

	m_cOrdinal = 0;

	m_points.clear ();
	m_points_extra.clear ();
	m_cTrackBeginPos = 0;
	m_strDefaultTrackLabel = _strMapFileName;

	m_pRtePoints = NULL;
	m_pRteRPoints = NULL;

	m_file_refs.clear ();
	m_file_refs.push_back (file_ref_t ());
	m_file_refs.back ().strFileName = _strMapFileName;
	m_file_refs.back ().bAttachment = true;

	m_pfnParseLat = NULL;
	m_pfnParseLon = NULL;

	SetProgressStatus (0);

	bool bFirst = true;
	const char * pLine = _pData;
	do {
		if (g_bStop)
			return lsInterrupted;

		const char * const pNextLine = FindNextLine (pLine, m_pEnd);

		if (pLine [0] != '\n' && pLine [0] != '\r') {
			switch (pLine [0]) {
				case 'H': // header (fields comment)
					ParseHeader (pLine, pNextLine);
					SaveTrack (pLine, pNextLine);
					m_cTrackBeginPos = pLine - m_pStart;
					break;

				case 'I': // information about file format
					break;

				case 'M': { // datum
					const char * p = pLine + 1;
					OmitSpaces (& p, pNextLine);
					if (HasPrefix (p, pNextLine, "G", 1)) {
						++ p;
						OmitSpaces (& p, pNextLine);
						if (HasPrefix (p, pNextLine, "WGS 84", 6))
							break;
						else
							ReportWarning ("Some unsupported datum is used. No datum conversion performed.");
					}
					break;
				}

				case 'U': { // units
					const char * p = pLine + 1;
					OmitSpaces (& p, pNextLine);
					if (HasPrefix (p, pNextLine, "LAT", 3)) {
						p += 3;
						OmitSpaces (& p, pNextLine);
						if (HasPrefix (p, pNextLine, "LON", 3)) {
							p += 3;
							OmitSpaces (& p, pNextLine);
							if (HasPrefix (p, pNextLine, "DEG", 3)) {
								m_pfnParseLat = _ParseDEGLat;
								m_pfnParseLon = _ParseDEGLon;
								break;
							} else if (HasPrefix (p, pNextLine, "DM", 2)) {
								m_pfnParseLat = _ParseDMLat;
								m_pfnParseLon = _ParseDMLon;
								break;
							}
						}
					}

					ReportWarning ("Unsupported units format! (Only Lat/Lon in degrees or degrees/minutes is supported.)");
					return lsFailed;
				}

				case 'R': // route
					if (bFirst) {
						bFirst = false;
						m_pMap->cHeaderLen = pLine - m_pStart;
					}
					ParseRoute (pLine, pNextLine);
					break;

				case 'W': // waypoint
					if (bFirst) {
						bFirst = false;
						m_pMap->cHeaderLen = pLine - m_pStart;
					}
					ParseWaypoint (pLine, pNextLine);
					break;

				case 'T': // track point
					if (bFirst) {
						bFirst = false;
						m_pMap->cHeaderLen = pLine - m_pStart;
						m_cTrackBeginPos = pLine - m_pStart;
					}
					ParseTrackpoint (pLine, pNextLine);
					break;
			}
		}

		pLine = pNextLine;

		SetProgressStatus (fast_floor ((50.f*(pLine - _pData))/_cFileSize + .5f));
	} while (pLine < _pData + _cFileSize);

	SetProgressStatus (50);

	SaveTrack (m_pEnd, m_pEnd);

	m_pMap->file_refs.swap (m_file_refs);

	// Suppress warning about empty Zoom etc.
	m_pMap->bNoHeader = true;

	return lsOK;
}